home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / lib / rhythmbox / plugins / artdisplay / AmazonCoverArtSearch.py < prev    next >
Encoding:
Python Source  |  2009-04-07  |  9.4 KB  |  294 lines

  1. # -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- 
  2. #
  3. # Copyright (C) 2006 - Gareth Murphy, Martin Szulecki
  4. #
  5. # This program is free software; you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation; either version 2, or (at your option)
  8. # any later version.
  9. #
  10. # The Rhythmbox authors hereby grant permission for non-GPL compatible
  11. # GStreamer plugins to be used and distributed together with GStreamer
  12. # and Rhythmbox. This permission is above and beyond the permissions granted
  13. # by the GPL license by which Rhythmbox is covered. If you modify this code
  14. # you may extend this exception to your version of the code, but you are not
  15. # obligated to do so. If you do not wish to do so, delete this exception
  16. # statement from your version.
  17. # This program is distributed in the hope that it will be useful,
  18. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  20. # GNU General Public License for more details.
  21. #
  22. # You should have received a copy of the GNU General Public License
  23. # along with this program; if not, write to the Free Software
  24. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
  25.  
  26. from xml.dom import minidom
  27. import re
  28. import locale
  29. import urllib
  30.  
  31. import rb
  32. import rhythmdb
  33.  
  34. LICENSE_KEY = "18C3VZN9HCECM5G3HQG2"
  35. DEFAULT_LOCALE = "en_US"
  36. ASSOCIATE = "webservices-20"
  37.  
  38. # We are not allowed to batch more than 2 requests at once
  39. # http://docs.amazonwebservices.com/AWSEcommerceService/4-0/PgCombiningOperations.html
  40. MAX_BATCH_JOBS = 2
  41.  
  42.  
  43. class Bag: pass
  44.  
  45. class AmazonCoverArtSearch (object):
  46.     def __init__ (self):
  47.         self.searching = False
  48.         self.cancel = False
  49.         self.db = None
  50.         self.entry = None
  51.         (self.tld, self.encoding) = self.__get_locale ()
  52.  
  53.     def __get_locale (self):
  54.         # "JP is the only locale that correctly takes UTF8 input. All other locales use LATIN1."
  55.         # http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1295&categoryID=117
  56.         supported_locales = {
  57.             "en_US" : ("com", "latin1"),
  58.             "en_GB" : ("co.uk", "latin1"),
  59.             "de" : ("de", "latin1"),
  60.             "ja" : ("jp", "utf8")
  61.         }
  62.  
  63.         lc_id = DEFAULT_LOCALE
  64.         default = locale.getdefaultlocale ()[0]
  65.         if default:
  66.             if supported_locales.has_key (default):
  67.                 lc_id = default
  68.             else:
  69.                 lang = default.split("_")[0]
  70.                 if supported_locales.has_key (lang):
  71.                     lc_id = lang
  72.  
  73.         return supported_locales[lc_id]
  74.  
  75.     def search (self, db, entry, on_search_completed_callback, *args):
  76.         self.searching = True
  77.         self.cancel = False
  78.         self.db = db
  79.         self.entry = entry
  80.         self.on_search_completed_callback = on_search_completed_callback
  81.         self.args = args
  82.         self.keywords = []
  83.  
  84.         st_artist = db.entry_get (entry, rhythmdb.PROP_ARTIST) or _("Unknown")
  85.         st_album = db.entry_get (entry, rhythmdb.PROP_ALBUM) or _("Unknown")
  86.  
  87.         if st_artist == st_album == _("Unknown"):
  88.             self.on_search_completed (None)
  89.             return
  90.  
  91.         # Tidy up
  92.  
  93.         # Replace quote characters
  94.         # don't replace single quote: could be important punctuation
  95.         for char in ["\""]:
  96.             st_artist = st_artist.replace (char, '')
  97.             st_album = st_album.replace (char, '')
  98.  
  99.  
  100.         self.st_album = st_album
  101.         self.st_artist = st_artist
  102.  
  103.         # Remove variants of Disc/CD [1-9] from album title before search
  104.         for exp in ["\([Dd]isc *[1-9]+\)", "\([Cc][Dd] *[1-9]+\)"]:
  105.             p = re.compile (exp)
  106.             st_album = p.sub ('', st_album)
  107.  
  108.         st_album_no_vol = st_album
  109.         for exp in ["\(*[Vv]ol.*[1-9]+\)*"]:
  110.             p = re.compile (exp)
  111.             st_album_no_vol = p.sub ('', st_album_no_vol)
  112.  
  113.         self.st_album_no_vol = st_album_no_vol
  114.  
  115.         # Save current search's entry properties
  116.         self.search_album = st_album
  117.         self.search_artist = st_artist
  118.         self.search_album_no_vol = st_album_no_vol
  119.         
  120.         # TODO: Improve to decrease wrong cover downloads, maybe add severity?
  121.         # Assemble list of search keywords (and thus search queries)
  122.         if st_album == _("Unknown"):
  123.             self.keywords.append ("%s Best of" % (st_artist))
  124.             self.keywords.append ("%s Greatest Hits" % (st_artist))
  125.             self.keywords.append ("%s Essential" % (st_artist))
  126.             self.keywords.append ("%s Collection" % (st_artist))
  127.             self.keywords.append ("%s" % (st_artist))
  128.         elif st_artist == _("Unknown"):
  129.             self.keywords.append ("%s" % (st_album))
  130.             if st_album_no_vol != st_artist:
  131.                 self.keywords.append ("%s" % (st_album_no_vol))
  132.             self.keywords.append ("Various %s" % (st_album))
  133.         else:
  134.             if st_album != st_artist:
  135.                 self.keywords.append ("%s %s" % (st_artist, st_album))
  136.                 if st_album_no_vol != st_album:
  137.                     self.keywords.append ("%s %s" % (st_artist, st_album_no_vol))
  138.                 self.keywords.append ("Various %s" % (st_album))
  139.             self.keywords.append ("%s" % (st_artist))
  140.  
  141.         # Initiate asynchronous search
  142.         self.search_next ()
  143.  
  144.     def search_next (self):
  145.         if len (self.keywords) == 0:
  146.             # No keywords left to search -> no results
  147.             self.on_search_completed (None)
  148.             return False
  149.  
  150.         self.searching = True
  151.  
  152.         url = "http://ecs.amazonaws." + self.tld + "/onca/xml" \
  153.               "?Service=AWSECommerceService"                   \
  154.               "&AWSAccessKeyId=" + LICENSE_KEY +               \
  155.               "&AssociateTag=" + ASSOCIATE +                   \
  156.               "&ResponseGroup=Images,ItemAttributes"           \
  157.               "&Operation=ItemSearch"                          \
  158.               "&ItemSearch.Shared.SearchIndex=Music"
  159.  
  160.         job = 1
  161.         while job <= MAX_BATCH_JOBS and len (self.keywords) > 0:
  162.             keyword = self.keywords.pop (0)
  163.             keyword = keyword.encode (self.encoding, "ignore")
  164.             keyword = keyword.strip ()
  165.             keyword = urllib.quote (keyword)
  166.             url += "&ItemSearch.%d.Keywords=%s" % (job, keyword)
  167.             job += 1
  168.  
  169.         # Retrieve search for keyword
  170.         l = rb.Loader()
  171.         l.get_url (url, self.on_search_response)
  172.         return True
  173.  
  174.     def __unmarshal (self, element):
  175.         rc = Bag ()
  176.         child_elements = [e for e in element.childNodes if isinstance (e, minidom.Element)]
  177.         if child_elements:
  178.             for child in child_elements:
  179.                 key = child.tagName
  180.                 if hasattr (rc, key):
  181.                     if not isinstance (getattr (rc, key), list):
  182.                         setattr (rc, key, [getattr (rc, key)])
  183.                     getattr (rc, key).append (self.__unmarshal (child))
  184.                 # get_best_match_urls() wants a list, even if there is only one item/artist
  185.                 elif child.tagName in ("Items", "Item", "Artist"):
  186.                     setattr (rc, key, [self.__unmarshal(child)])
  187.                 else:
  188.                     setattr (rc, key, self.__unmarshal(child))
  189.         else:
  190.             rc = "".join ([e.data for e in element.childNodes if isinstance (e, minidom.Text)])
  191.         return rc
  192.  
  193.     def on_search_response (self, result_data):
  194.         if result_data is None:
  195.             self.search_next()
  196.             return
  197.  
  198.         try:
  199.             xmldoc = minidom.parseString (result_data)
  200.         except:
  201.             self.search_next()
  202.             return
  203.         
  204.         data = self.__unmarshal (xmldoc)
  205.         if not hasattr (data, "ItemSearchResponse") or \
  206.            not hasattr (data.ItemSearchResponse, "Items"):
  207.             # Something went wrong ...
  208.             self.search_next ()
  209.         else:
  210.             # We got some search results
  211.             self.on_search_results (data.ItemSearchResponse.Items)
  212.  
  213.     def on_search_results (self, results):
  214.         self.on_search_completed (results)
  215.  
  216.     def on_search_completed (self, result):
  217.         self.on_search_completed_callback (self, self.entry, result, *self.args)
  218.         self.searching = False
  219.  
  220.     def __tidy_up_string (self, s):
  221.         # Lowercase
  222.         s = s.lower ()
  223.         # Strip
  224.         s = s.strip ()
  225.  
  226.         # TODO: Convert accented to unaccented (fixes matching Salom√© vs Salome)
  227.         s = s.replace (" - ", " ")    
  228.         s = s.replace (": ", " ")
  229.         s = s.replace (" & ", " and ")
  230.  
  231.         return s
  232.  
  233.     def __valid_match (self, item):
  234.         return (hasattr (item, "LargeImage") or hasattr (item, "MediumImage")) \
  235.                and hasattr (item, "ItemAttributes")
  236.  
  237.     def get_best_match_urls (self, search_results):
  238.         # Default to "no match", our results must match our criteria
  239.         best_match = None
  240.  
  241.         for result in search_results:
  242.             if not hasattr (result, "Item"):
  243.                 # Search was unsuccessful, try next batch job
  244.                 continue
  245.  
  246.             items = filter(self.__valid_match, result.Item)
  247.             if self.search_album != _("Unknown"):
  248.                 album_check = self.__tidy_up_string (self.search_album)
  249.                 for item in items:
  250.                     if not hasattr (item.ItemAttributes, "Title"):
  251.                         continue
  252.  
  253.                     album = self.__tidy_up_string (item.ItemAttributes.Title)
  254.                     if album == album_check:
  255.                         # Found exact album, can not get better than that
  256.                         best_match = item
  257.                         break
  258.                     # If we already found a best_match, just keep checking for exact one
  259.                     # Check the results for both an album name that contains the name
  260.                     # we're searching for, and an album name that's a substring of the
  261.                     # name we're searching for
  262.                     elif (best_match is None) and \
  263.                          (album.find (album_check) != -1 or
  264.                           album_check.find (album) != -1):
  265.                         best_match = item
  266.  
  267.             # If we still have no definite hit, use first result where artist matches
  268.             if (self.search_album == _("Unknown") and self.search_artist != _("Unknown")):
  269.                 artist_check = self.__tidy_up_string (self.search_artist)
  270.                 if best_match is None:
  271.                     # Check if artist appears in the Artists list
  272.                     hit = False
  273.                     for item in items:
  274.                         if not hasattr (item.ItemAttributes, "Artist"):
  275.                             continue
  276.  
  277.                         for artist in item.ItemAttributes.Artist:
  278.                             artist = self.__tidy_up_string (artist)
  279.                             if artist.find (artist_check) != -1:
  280.                                 best_match = item
  281.                                 hit = True
  282.                                 break
  283.                         if hit:
  284.                             break
  285.  
  286.             urls = [getattr (best_match, size).URL for size in ("LargeImage", "MediumImage")
  287.                     if hasattr (best_match, size)]
  288.             if urls:
  289.                 return urls
  290.  
  291.         # No search was successful
  292.         return []
  293.